package boardgamer.yavalath.common.ai;

import boardgamer.common.net.PlayerId;
import boardgamer.yavalath.common.GeneratesMove;
import boardgamer.yavalath.common.YavalathException;
import boardgamer.yavalath.common.YavalathGameState;
import boardgamer.yavalath.common.YavalathGameState.Phase;
import boardgamer.yavalath.common.YavalathGameStateChange;
import boardgamer.yavalath.common.YavalathGameStateChange.Event;
import boardgamer.yavalath.common.YavalathTile;
import boardgamer.yavalath.server.YavalathGameStateUpdater;
import boardgamer.yavalath.server.YavalathGameStateUpdater.Pair;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;

import java.util.Random;

/**
 * Steven Fraim (stevenfraim@gmail.com)
 *
 * http://hal.inria.fr/docs/00/12/15/16/PDF/RR-6062.pdf
 */
public class UCTAi implements GeneratesMove {
  private static final double UCTK = .44;
  // Number of simulations untill children are expanded.
  private static final Random RANDOM = new Random(System.nanoTime());
  private static final int NUM_SIMULATIONS = 1;
  private static final int WIN = 1;
  private static final int DRAW = 0;
  private static final int LOSS = -1;
  private static final YavalathTile ROOT_TILE = new YavalathTile(-1, -1);
  private static final RandomAi RANDOM_AI = new RandomAi();

  private PlayerId currentAiPlayer;
  
  @Override
  public YavalathTile getMove(YavalathGameState state) throws YavalathException {
    System.out.println("Getting move");
    currentAiPlayer = state.getCurrentPlayer();

    return UCTSearch(state.getBoard().getUnownedTilesCount() /* play each tile at least once */, state);
  }

  private YavalathTile UCTSearch(int numSimulations, YavalathGameState state) throws YavalathException {
    Node root = new Node(ROOT_TILE, YavalathGameState.Builder.fromPrototype(state).build());
    createChildren(root);

    for (int i = 0; i < numSimulations; i++) {
      playSimulation(root);
    }

    Node n = getBestChild(root);
    return new YavalathTile(n.getYavalathTile().getX(), n.getYavalathTile().getY());
  }

  /**
   * Generates all legal successors nodes given the current node. Stores these nodes as children to {@code parent}.
   */
  private void createChildren(Node parent) throws YavalathException {

    ImmutableList.Builder<Node> childrenBuilder = ImmutableList.builder();
    for (YavalathTile tile : parent.getYavalathGameState().getBoard().getTiles()) {
      if (!tile.hasOwner()) {
        childrenBuilder.add(new Node(tile, makeMove(parent.getYavalathGameState(), tile.getX(), tile.getY())));
      }
    }
    parent.setChildren(childrenBuilder.build());
  }

  private YavalathGameState makeMove(YavalathGameState state, int x, int y) throws YavalathException {
    return Iterables.getLast(YavalathGameStateUpdater.getUpdatedGameStateAddToken(state,
        state.getCurrentPlayer(), x, y)).first;
  }

  private int playSimulation(Node node) throws YavalathException {
    int randomResult;
    if (node.getChildren() == null && node.getVisits() < NUM_SIMULATIONS) {
      randomResult = playRandomGame(node);
    } else {
      if (node.getChildren() == null) {
        createChildren(node);
      }

      randomResult = -1 * playSimulation(UCTSelect(node));
    }
    node.update(randomResult);
    return randomResult;
  }

  /**
   * This method plays a random game and returns win, draw, or loss.
   */
  private int playRandomGame(Node node) throws YavalathException {
    try {
      Pair<YavalathGameState, YavalathGameStateChange> lastPair = null;
      YavalathGameState state = node.getYavalathGameState();

      while (state.getPhase() != Phase.END_GAME) {
        YavalathTile tile = RANDOM_AI.getMove(state);
        lastPair = Iterables.getLast(YavalathGameStateUpdater.getUpdatedGameStateAddToken(state, state.getCurrentPlayer(),
            tile.getX(), tile.getY()));
        state = lastPair.first;
      }
    
      // Refactor to clean this logic up
      if ((lastPair == null && node.getYavalathGameState().getActivePlayers().size() == 1)
          || lastPair != null && lastPair.second.getEvent().equals(Event.PLAYER_WINS)) {
        return lastPair == null
            ? state.getCurrentPlayer().equals(currentAiPlayer) ? WIN : LOSS
            : lastPair.first.getCurrentPlayer().equals(currentAiPlayer) ? WIN : LOSS;
      } else if ((lastPair == null && node.getYavalathGameState().getActivePlayers().size() == 0)
          || lastPair != null && lastPair.second.getEvent().equals(Event.DRAW)) {
        return DRAW;
      } else {
        throw new IllegalStateException("Illegal state");
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private Node UCTSelect(Node parent) {
    Node bestNode = null;
    double bestUct = 0;

    for (Node node : parent.getChildren()) {
      double uctValue;

      if (node.getVisits() > 0) {
        double uct = UCTK * Math.sqrt( Math.log(parent.getVisits()) / (double) node.getVisits() );
        uctValue = node.getWinRate() + uct;
      } else {
        uctValue = 10000 + 1000 * RANDOM.nextDouble();
      }

      if (uctValue > bestUct) {
        // get max uctValue of all children
        bestUct = uctValue;
        bestNode = node;
      }
    }
    return bestNode;
  }

  /**
   * Returns child node with the best win ratio.
   */
  private Node getBestChild(Node root) {
    //checkState(!root.getChildren().isEmpty(), "No children");
    return Ordering.natural().max(root.getChildren());
  }
}